<?php
/* vim: set expandtab tabstop=4 shiftwidth=4: */
// +----------------------------------------------------------------------+
// | PHP version 4                                                        |
// +----------------------------------------------------------------------+
// | Copyright (c) 1997-2002 The PHP Group                                |
// +----------------------------------------------------------------------+
// | This source file is subject to version 2.0 of the PHP license,       |
// | that is bundled with this package in the file LICENSE, and is        |
// | available through the world-wide-web at                              |
// | http://www.php.net/license/2_02.txt.                                 |
// | If you did not receive a copy of the PHP license and are unable to   |
// | obtain it through the world-wide-web, please send a note to          |
// | license@php.net so we can mail you a copy immediately.               |
// +----------------------------------------------------------------------+
// | Author: Xavier Noguer <xnoguer@php.net>                              |
// +----------------------------------------------------------------------+
//
// $Id: DICOM.php,v 0.3.1 2003/12/14 19:18:17 xnoguer Exp $

/*
 * Contributions to version 0.3 by Nano Documet <nanodocumet@gmail.com>
 * ChangeLog:
 * 2007.07.31: Now the htmlParse function does not need an object for
 *             parameter one.
 * Version 0.3.1
 * 2008.04.07: Added support for color images
 */

require_once('PEAR.php');
require_once('DICOM/Element.php');
require_once('DICOM/Dictionary.php');
$dictionary = new File_DICOM_Dictionary();

define('FILE_DICOM_VR_TYPE_EXPLICIT_32_BITS', 0);
define('FILE_DICOM_VR_TYPE_EXPLICIT_16_BITS', 1);
define('FILE_DICOM_VR_TYPE_IMPLICIT', 2);
define('IMPLICIT_VR_LITTLE_ENDIAN',"1.2.840.10008.1.2");
define('EXPLICIT_VR_LITTLE_ENDIAN',"1.2.840.10008.1.2.1");
define('EXPLICIT_VR_BIG_ENDIAN',"1.2.840.10008.1.2.2");

/**
* Value Representations (DICOM Standard PS 3.5 Sect 6.2)
* Bytes=0 => Undefined length.
* Fixed=1 => Exact field length, otherwise max length.
* each array contains:  Name, Bytes, Fixed
* @var array
*/

    $VR_array = array(
'AE' => array('Application Entity',16,0),
'AS' => array('Age String',4,1),
'AT' => array('Attribute Tag',4,1),
'CS' => array('Code String',16,0),
'DA' => array('Date',8,1),
'DS' => array('Decimal String',16,0),
'DT' => array('Date Time',26,0),
'FL' => array('Floating Point Single',4,1),
'FD' => array('Floating Point Double',8,1),
'IS' => array('Integer String',12,0),
'LO' => array('Long String',64,0),
'LT' => array('Long Text',10240,0),
'OB' => array('Other Byte String',0,0),
'OX' => array('Mixed. Other {Byte|Word} String',0,0),
'OW' => array('Other Word String',0,0),
'PN' => array('Person Name',64,0),
'SH' => array('Short String',16,0),
'SL' => array('Signed Long',4,1),
'SQ' => array('Sequence of Items',0,0),
'SS' => array('Signed Short',2,1),
'ST' => array('Short Text',1024,0),
'TM' => array('Time',16,0),
'UI' => array('Unique Identifier UID',64,0),
'UL' => array('Unsigned Long',4,1),
'UN' => array('Unknown',0,0),
'US' => array('Unsigned Short',2,1),
'UT' => array('Unlimited Text',0,0)
);

/**
* This class allows reading and modifying of DICOM files
*
* @author   Xavier Noguer <xnoguer@php.net>
* @package  File_DICOM
*/
class File_DICOM extends PEAR
{
    /**
    * DICOM dictionary.
    * @var array
    */
    var $dict;

    /**
    * Flag indicating if the current file is a DICM file or a NEMA file.
    * true => DICM, false => NEMA.
    *
    * @var bool
    */
    var $is_dicm;

    /**
    * Currently open file.
    * @var string
    */
    var $current_file;

    /**
    * Initial 0x80 bytes of last read file
    * @var string
    */
    var $_preamble_buff;

    /**
    * Array of DICOM elements indexed by group and element index
    * @var array
    */
    var $_elements;

    /**
    * Array of DICOM elements indexed by name
    * @var array
    */
    var $_elements_by_name;

    /**
    * Transfer Syntax. To be able of reading different types according to standard
    * 1.2.840.10008.1.2    Implicit VR, Little Endian
    * 1.2.840.10008.1.2.1  Explicit VR, Little Endian
    * 1.2.840.10008.1.2.2  Explicit VR, Big Endian
    * @var string
    */
    var $transfer_syntax;

    /**
    * Encapsulated Transfer Syntax.
    * 1.2.840.10008.1.2.4.50 JPEG baseline
    * 1.2.840.10008.1.2.4.51 JPEG extended
    * 1.2.840.10008.1.2.4.57 JPEG lossless, non-hierarchical
    * 1.2.840.10008.1.2.4.70 JPEG lossless, non-hierarchical, first-order prediction
    * @var string
    */
    var $_encapsulated_transfer_syntax = array(
    "1.2.840.10008.1.2.4.50",
    "1.2.840.10008.1.2.4.51",
    "1.2.840.10008.1.2.4.57",
    "1.2.840.10008.1.2.4.70"
    );

    /**
    * Constructor.
    * It creates a File_DICOM object.
    *
    * @access public
    */
    function File_DICOM()
    {
        /**
        * Initialize dictionary.
		*/
		        global $dictionary;
		        $this->dict = $dictionary->dict;
    }

    /**
    * Utility to know if a file is DICOM or not.
    * It is not a bullet-proof test.
    *
    * @access public
    * @param string $infile The DICOM file to parse
    * @return boolean true if file is DICOM, or false is file does not satisfy the preamble test.
    *                 Notice, that a file returning false could still be DICOM.
    */
    function isDicom($infile) {
        return $this->parse($infile, true);
    }

    /**
    * Parse a DICOM file
    * Parse a DICOM file and get all of its header members
    *
    * @access public
    * @param string $infile The DICOM file to parse
    * @param boolean $only_check_type Default to false. It is true when called by method isDicom
    * @return mixed true on success or DICOM file, PEAR_Error on failure, or false when is not a DICOM file
    */
    function parse($infile, $only_check_type = false)
    {
        $this->current_file = $infile;
        // Fill the transfer syntax by default to Explicit VR Little Endian
        $this->transfer_syntax = EXPLICIT_VR_LITTLE_ENDIAN;
        $fh = @fopen($infile, "rb");
        if (!$fh) {
            return $this->raiseError("Could not open file $infile for reading");
        }
        $stat = fstat($fh);
        $this->_file_length = $stat[7];

        // Test for NEMA or DICOM file.
        // If DICM, store initial preamble and leave file ptr at 0x84.
        $this->_preamble_buff = fread($fh, 0x80);
        $buff = fread($fh, 4);
        $this->is_dicm = ($buff == 'DICM');

        if ($only_check_type) {
            fclose($fh);
            return $this->is_dicm;
        }

        if (!$this->is_dicm) {
            fseek($fh, 0);
        }

        $last_group = 0x0000;
        // Fill in hash with header members from given file.
        while (ftell($fh) < $this->_file_length)
        {
            $element =& new File_DICOM_Element($fh, $this->dict,$this->transfer_syntax,$last_group);
            $this->_elements[$element->group][$element->element][] =& $element;
            $this->_elements_by_name[$element->name][] =& $element;
            $last_group = $element->group;
            // Fill the proper transfer syntax
            if ($element->group == 0x0002 && $element->element == 0x0010) {
            	$this->transfer_syntax = $element->value;
            }
        }
        fclose($fh);
        return true;
    }

    /**
    * Write current contents to a DICOM file.
    *
    * @access public
    * @param string $outfile The name of the file to write. If not given
    *                        it assumes the name of the file parsed.
    *                        If no file was parsed and no name is given
    *                        returns a PEAR_Error
    * @return mixed true on success, PEAR_Error on failure
    */
    function write($outfile = '')
    {
        if ($outfile == '') {
            if (isset($this->current_file)) {
                $outfile = $this->current_file;
            } else {
                return $this->raiseError("File name not given (and no file currently open)");
            }
        }
        $fh = @fopen($outfile, "wb");
        if (!$fh) {
            return $this->raiseError("Could not open file $outfile for writing");
        }

        // Writing file from scratch will always fail for now
        if (!isset($this->_preamble_buff)) {
            return $this->raiseError("Cannot write DICOM file from scratch");
        }
        // Don't store initial preamble and DICM word. Working with NEMA.
        //fwrite($fh, $this->_preamble_buff);
        //fwrite($fh, 'DICM');
        /*$buff = fread($fh, 4);
        $this->is_dicm = ($buff == 'DICM');
        if (!$this->is_dicm) {
            fseek($fh, 0);
        }*/

        // There are not that much groups/elements. Using foreach()
        foreach (array_keys($this->_elements) as $group) {
            foreach (array_keys($this->_elements[$group]) as $element) {
            	foreach (array_keys($this->_elements[$group][$element]) as $z) {
                    fwrite($fh, pack('v', $group));
                    fwrite($fh, pack('v', $element));
                    $code = $this->_elements[$group][$element][$z]->code;
                    // Preserve the VR type from the file parsed
                    if (($this->_elements[$group][$element][$z]->vr_type == FILE_DICOM_VR_TYPE_EXPLICIT_32_BITS) or ($this->_elements[$group][$element][$z]->vr_type == FILE_DICOM_VR_TYPE_EXPLICIT_16_BITS)) {
                        fwrite($fh, pack('CC', $code{0}, $code{1}));
                        // This is an OB, OW, SQ, UN or UT: 32 bit VL field.
                        if ($this->_elements[$group][$element][$z]->vr_type == FILE_DICOM_VR_TYPE_EXPLICIT_32_BITS) {
                            fwrite($fh, pack('V', $this->_elements[$group][$element][$z]->length));
                        } else { // not using fixed length from VR!!!
                            fwrite($fh, pack('v', $this->_elements[$group][$element][$z]->length));
                        }
                    } elseif ($this->_elements[$group][$element][$z]->vr_type == FILE_DICOM_VR_TYPE_IMPLICIT) {
                        fwrite($fh, pack('V', $this->_elements[$group][$element][$z]->length));
                    }
                    switch ($code) {
                    // Decode unsigned longs and shorts.
                    case 'UL':
                        fwrite($fh, pack('V', $this->_elements[$group][$element][$z]->value));
                        break;
                    case 'US':
                        fwrite($fh, pack('v', $this->_elements[$group][$element][$z]->value));
                        break;
                    // Floats: Single and double precision.
                    case 'FL':
                        fwrite($fh, pack('f', $this->_elements[$group][$element][$z]->value));
                        break;
                    case 'FD':
                        fwrite($fh, pack('d', $this->_elements[$group][$element][$z]->value));
                        break;
                    // Binary data. Only save position. Is this right?
                    case 'OW':
                    case 'OB':
                    case 'OX':
                        // Binary data. Value only contains position on the parsed file.
                        // Will fail when file name for writing is the same as for parsing.
                        $fh2 = @fopen($this->current_file, "rb");
                        if (!$fh2) {
                            return $this->raiseError("Could not open file {$this->current_file} for reading");
                        }
                        fseek($fh2, $this->_elements[$group][$element][$z]->value);
                        fwrite($fh, fread($fh2, $this->_elements[$group][$element][$z]->length));
                        fclose($fh2);
                        break;
                    default:
                        fwrite($fh, $this->_elements[$group][$element][$z]->value, $this->_elements[$group][$element][$z]->length);
                        break;
                    }
                }
            }
        }
        fclose($fh);
        return true;
    }

    /**
    * Gets the value for a DICOM element
    * Gets the value for a DICOM element of a given group from the
    * parsed DICOM file.
    *
    * @access public
    * @param mixed $gp_or_name The group the DICOM element belongs to
    *                          (integer), or it's name (string)
    * @param integer $el       The identifier for the DICOM element
    *                          (unique inside a group)
    * @param integer $z       The 3rd dimension for the element (created for sequences)
    *                         By default is set to 0 for backwards compatibility
    *                         (usually for single elements).
    * @return mixed The value for the DICOM element on success, PEAR_Error on failure
    */
    function getValue($gp_or_name, $el = null, $z = 0)
    {
        if (isset($el)) // retreive by group and element index
        {
            if (isset($this->_elements[$gp_or_name][$el][$z])) {
                $value = $this->_elements[$gp_or_name][$el][$z]->getValue();
                if (empty($value)) {
                    return null;
                }
                return $this->_elements[$gp_or_name][$el][$z]->getValue();
            }
            else {
                $this->raiseError("Element ($gp_or_name,$el) not found");
                return null;
            }
        }
        else // retrieve by name
        {
            if (isset($this->_elements_by_name[$gp_or_name][$z])) {
                $value = $this->_elements_by_name[$gp_or_name][$z]->getValue();
                if (empty($value)) {
                    return null;
                }
                return $this->_elements_by_name[$gp_or_name][$z]->getValue();
            }
            else {
                $this->raiseError("Element $gp_or_name not found");
                return null;
            }
        }
    }

    /**
    * Sets the value for a DICOM element
    * Only works with strings now.
    *
    * @access public
    * @param integer $gp The group the DICOM element belongs to
    * @param integer $el The identifier for the DICOM element (unique inside a group)
    * @param integer $z The index of the element. By default 0 (for single elements)
    */
    function setValue($gp, $el, $value, $z = 0)
    {
        $this->_elements[$gp][$el][$z]->value = $value;
        $this->_elements[$gp][$el][$z]->length = strlen($value);
    }

    /**
    * Dumps the contents of the image inside the DICOM file
    * (element 0x0010 from group 0x7FE0) to a PGM (Portable Gray Map) file.
    * Use with Caution!!. For a 8.5MB DICOM file on a P4 it takes 28
    * seconds to dump it's image.
    *
    * @access public
    * @param string  $filename The file where to save the image
    * @return mixed true on success, PEAR_Error on failure.
    */
    function dumpImage($filename)
    {
        $length = $this->_elements[0x7FE0][0x0010]->length;
        $rows = $this->getValue(0x0028,0x0010);
        $cols = $this->getValue(0x0028,0x0011);
        $fh = @fopen($filename, "wb");
        if (!$fh) {
            return $this->raiseError("Could not open file $filename for writing");
        }
        // magick word
        fwrite($fh, "P5\n");
        // comment
        fwrite($fh, "# file generated by PEAR::File_DICOM on ".strftime("%Y-%m-%d", time())." \n");
        fwrite($fh, "# do not use for diagnosing purposes\n");
        fwrite($fh, "$cols $rows\n");
        // always 255 grays
        fwrite($fh, "255\n");
        $pos = $this->getValue(0x7FE0,0x0010);
        $fh_in = @fopen($this->current_file, "rb");
        if (!$fh_in) {
            return $this->raiseError("Could not open file {$this->current_file} for reading");
        }
        fseek($fh_in, $pos);
        $block_size = 4096;
        $blocks = ceil($length / $block_size);
        for ($i = 0; $i < $blocks; $i++) {
            if ($i == $blocks - 1) { // last block
                $chunk_length = ($length % $block_size) ? ($length % $block_size) : $block_size;
            } else {
                $chunk_length = $block_size;
            }
            $chunk = fread($fh_in, $chunk_length);
            $pgm = '';
            $half_chunk_length = $chunk_length/2;
            $rr = unpack("v$half_chunk_length", $chunk);
            for ($j = 1; $j <= $half_chunk_length; $j++) {
                $pgm .= pack('C', $rr[$j] >> 4);
            }
            fwrite($fh, $pgm);
        }
        fclose($fh_in);
        fclose($fh);
        return true;
    }

    /**
    * Creates an array of GD image object.
    *
    * @access public
    * @param string  $filename The file where to save the image
    * @return mixed image object array on success,
    *               PEAR_Error on failure or NULL if GD is not configured.
    */
    function imagecreatefromDICOM($filename, $window = null, $level = null)
    {
        if (!function_exists('imagecreatetruecolor')) {
            return NULL;
        }

        // Let's read some values from DICOM file
        $length            = $this->_elements[0x7FE0][0x0010][0]->length;
        $rows              = $this->getValue(0x0028, 0x0010);
        $cols              = $this->getValue(0x0028, 0x0011);
        $samples_per_pixel = $this->getValue(0x0028, 0x0002);
        $bits              = $this->getValue(0x0028, 0x0100);
        $high_bit          = $this->getValue(0x0028, 0x0102);
        $dose_scaling      = (empty($this->_elements[0x3004][0x000E][0]->value))? 1 : $this->getValue(0x3004, 0x000E);
        $window_center     = (empty($this->_elements[0x0028][0x1050][0]->value))? 0 : $this->getValue(0x0028, 0x1050);
        $window_width      = (empty($this->_elements[0x0028][0x1051][0]->value))? 0 : $this->getValue(0x0028, 0x1051);
        $rescale_intercept = (empty($this->_elements[0x0028][0x1052][0]->value))? 0 : $this->getValue(0x0028, 0x1052);
        $rescale_slope     = (empty($this->_elements[0x0028][0x1053][0]->value))? 1 : $this->getValue(0x0028, 0x1053);
        $number_of_frames  = (empty($this->_elements[0x0028][0x0008][0]->value))? 1 : $this->getValue(0x0028, 0x0008);
        $pixel_representation       = $this->getValue(0x0028, 0x0103);
        $photometric_interpretation = $this->getValue(0x0028, 0x0004);
        $starting_position          = $this->getValue(0x7FE0, 0x0010);
        $transfer_syntax_uid        = $this->getValue(0x0002, 0x0010);
        //$this->_elements = null;

        if (in_array($transfer_syntax_uid, $this->_encapsulated_transfer_syntax)) {
            // Sorry, encapsulated data is more complex to display... by now.
            return NULL;
        }

        if ($rows * $cols >= 500000) {
            // Sorry, file is too big, conver it in a different way
            return NULL;
        }

        // Window Center and Width can have multiple values. By now, just reading the first one. It assumes the delimiter
        // is the "\"
        if (!(strpos($window_center,"\\") === false)) {
            $temp          = explode("\\",$window_center);
            $window_center = intval($temp[0]);
        }
        if (!(strpos($window_width,"\\") === false)) {
            $temp          = explode("\\",$window_width);
            $window_width = intval($temp[0]);
        }

        // Opening the file
        $fh = fopen ($filename, 'rb');
        if (!$fh) {
            return $this->raiseError("Could not open file $filename for writing");
        }

        fseek($fh, $starting_position);


        // $data holds the pixel values
        $maximum = array();
        $minimum = array();
        $images  = array();
        $data    = array();
        $max     = array();
        $min     = array();
        $current_position = $starting_position;
        $current_image    = 0;
        $bytes_to_read = $bits/8;
        $size_image = $cols * $rows * $bytes_to_read * $samples_per_pixel;
        $length = $size_image * $number_of_frames;

        while (ftell($fh) < $starting_position + $length) {
            if ($current_position == $starting_position + $current_image*$size_image) {
                $x   = 0;
                $y   = 0;
                for ($i = 0; $i < $samples_per_pixel; $i++) {
                    $max[$current_image][$i] = -20000; // Small enough so it will be properly calculated
                    $min[$current_image][$i] = 20000;  // Large enough so it wil be properly calculated
                }
                $current_image++;
            }
            for ($i = 0; $i < $samples_per_pixel; $i++) {
                $chunk = fread($fh, $bytes_to_read);
                $current_position += $bytes_to_read;
                $pixel_value = base_convert(bin2hex(strrev($chunk)), 16, 10);
                // Now we have the pixel value
                // Checking if 2's complement
                $pixel_value = ($pixel_representation)? $this->complement2($pixel_value, $high_bit) : $pixel_value;
                // Getting the right value according to slope and intercept
                $pixel_value = $pixel_value*$rescale_slope + $rescale_intercept;
                // Multiplying for dose_grid_scaling
                $pixel_value = $pixel_value*$dose_scaling;
                // Assigning the value
                $data[$current_image - 1][$x][$y][$i] = $pixel_value;

                // Getting the max
                if ($pixel_value > $max[$current_image - 1][$i]) {
                    $max[$current_image - 1][$i] = $pixel_value; // max
                }
                // Getting the min
                if ($pixel_value < $min[$current_image - 1][$i]) {
                    $min[$current_image - 1][$i] = $pixel_value; // min
                }
            }
            $y++;

            if ($y == $cols)  { // Next row
                $x++;
                $y=0;
            }
        }
        fclose ( $fh );

		        for ($index = 0; $index < $current_image; $index++) {
            for ($i = 0; $i < $samples_per_pixel; $i++) {
                // Real max and min according to window center & width (if set)
                $maximum[$i] = ($window_center != 0 && $window_width != 0)? round($window_center + $window_width/2) : $max[$index][$i];
                $minimum[$i] = ($window_center != 0 && $window_width != 0)? round($window_center - $window_width/2) : $min[$index][$i];

                // Check if window and level are sent
                $maximum[$i] = (!empty($window) && !empty($level))? round($level + $window/2) : $maximum[$i];
                $minimum[$i] = (!empty($window) && !empty($level))? round($level - $window/2) : $minimum[$i];
                //echo $index . " " . $maximum . " " . $minimum . "<br />\n";
                if ($maximum[$i] == $minimum[$i]) { // Something wrong. Avoid having a zero division
                    return NULL;
                }
            }
            $img = imagecreatetruecolor($cols, $rows);
            for($x =0; $x < $rows;$x++) {
                for ($y = 0; $y < $cols; $y++) {
                    $pixel_values = array();
                    for ($i = 0; $i < $samples_per_pixel; $i++) {
                        $pixel_value = $data[$index][$x][$y][$i];
                        // truncating pixel values over max and below min
                        $pixel_value = ($pixel_value > $maximum[$i])? $maximum[$i] : $pixel_value;
                        $pixel_value = ($pixel_value < $minimum[$i])? $minimum[$i] : $pixel_value;
                        // Converting to gray value
                        $pixel_value = ($pixel_value - $minimum[$i])/($maximum[$i] - $minimum[$i])*255;
                        // For MONOCHROME1 we have to invert the pixel values.
                        if ($photometric_interpretation == "MONOCHROME1") {
                            $pixel_value = 255 - $pixel_value;
                        }
                        $pixel_values[$i] = $pixel_value;
                    }
                    if ($samples_per_pixel == 1) {
                        $color = imagecolorallocate($img, $pixel_values[0], $pixel_values[0], $pixel_values[0]);
                    }
                    else {
                        $color = imagecolorallocate($img, $pixel_values[0], $pixel_values[1], $pixel_values[2]);
                    }
                    imagesetpixel($img, $y, $x, $color);
                }
            }
            $images[] = $img;
		        }
        return $images;
    }

    /**
    * Return an integer that was saved as complement 2 .
    *
    * @access public
    * @param integer  $number The integer number to convert
    * @param integer  $high_bit The high bit for the value. By default 15 (assumes 2 bytes)
    * @return integer the number after complement's 2 applied
    */
    function complement2($number, $high_bit = 15)
    {
        $sign = $number >> $high_bit;
        if ($sign) { // Negative
            $number = pow(2, $high_bit + 1) - $number;
            $number = -1 * $number;
        }
        return $number;
    }

    /**
    * Returns an html string with the DICOM file parsed.
    *
    * @access public
    * @param object   $dicom_element A Dicom element. It could be a sequence too.
    * @param string   $prefix To have a nice display at the beginning of each line
    * @return string  An html string
    */
    function htmlParse(&$dicom_element = "", $prefix = "Element")
    {
        if ($dicom_element == "") {
            $dicom_element = $this;
        }
        $data = "";
        $count = 1;
        if (count($dicom_element->_elements) > 0) {
            foreach (array_keys($dicom_element->_elements) as $group) {  // Loading groups
                foreach (array_keys($dicom_element->_elements[$group]) as $element) { // Loading elements
                    foreach ($dicom_element->_elements[$group][$element] as $thisElement) { // Loading items in element (Sequences or single element)
                        $value = ($thisElement->name == "UNKNOWN")? "Propriatory": $thisElement->value;
                        $data .= "<span style=\"color:blue;\"> $prefix.$count - ".$thisElement->name . "</span> <span style=\"color:red;\">[" . $this->strHex($group) . "][" . $this->strHex($element) . "] <strong>(Length: ". $thisElement->length . ")</strong></span> = " . $value . "<br />";
                        $data .= $this->htmlParse($thisElement,"$prefix.$count");
                        $count++;
                    }
                }
            }
        }
        return $data;
    }

    /**
    * Returns an string as 0x0000. This might be done also using sprintf or alike but not so familiar with that.
    *
    * @access public
    * @param integer  $value An integer value. Either the group or element number.
    * @return string  $string An string with the format 0x0000.
    */
    function strHex($value)
    {
        $string = "0x" . str_pad(strtoupper(dechex($value)), 4, "0", STR_PAD_LEFT);
        return $string;
    }
}
?>